﻿using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Types;
using MyFloridaHouse.libSvDTO;
using System;
using MyFloridaHouse.libSvCommonUtilities;

namespace MyFloridaHouse.libServiceBusinessRules
{
    /// <summary>
    /// This class provides general functions for working w/ sql geometry data type
    /// </summary>
    public static class SQLGeometryTools
    {

        internal static SqlGeometry BuildDefaultDistrictShape()
        {
            // make a very tiny polygon
            double offset = .000001;
            
            double north = Constants.WKT_EMPTY_SHAPE_LATITUDE+offset;
            double south = Constants.WKT_EMPTY_SHAPE_LATITUDE-offset;

            double east = Constants.WKT_EMPTY_SHAPE_LONGITUDE-offset;
            double west = Constants.WKT_EMPTY_SHAPE_LONGITUDE+offset;
            
            StringBuilder sb = new StringBuilder();
            sb.Append("POLYGON((");
            sb.Append(west.ToString() + " " + south.ToString() + ",");
            sb.Append(east.ToString() + " " + south.ToString() + ",");
            sb.Append(east.ToString() + " " + north.ToString() + ",");
            sb.Append(west.ToString() + " " + north.ToString() + ",");
            sb.Append(west.ToString() + " " + south.ToString() );
            sb.Append("))");
            
            SqlChars sc = new SqlChars(sb.ToString());
            SqlGeometry newGeom = SqlGeometry.STGeomFromText(sc, 4326);
            
            return newGeom;
            
        }

        /// <summary>
        /// Gets the label location.
        /// See the sql spatial fourum at http://social.msdn.microsoft.com/Forums/en-US/sqlspatial/thread/4e9f687e-ee26-437a-9b71-765d381171b6
        /// The methodology used is to get the two lines that intersect the shape centered on the bounding box
        /// and determine which one is the best fit.  If not best fit is found within the shape based on this method
        /// then the centroid is returned
        /// </summary>
        /// <param name="geom">The geom.</param>
        /// <returns></returns>
        public static PointDTO GetLabelLocation(SqlGeometry geom)
        {
            SqlGeometry returnGeom = new SqlGeometry() ;
            PointDTO returnPoint= new PointDTO();
            SqlGeometry labelPtGeom; 
            SqlGeometry boundingBox = geom.STEnvelope();
            //First find the latitudinal label point
            var NWLat = (double)boundingBox.STPointN(4).STY;
            var NWLon = (double)boundingBox.STPointN(4).STX;
            var SELat = (double)boundingBox.STPointN(2).STY;
            var SELon = (double)boundingBox.STPointN(2).STX;
            var midY = ((NWLat - SELat)/2) + SELat;
            var midX = ((NWLon - SELon)/2) + SELon;
            var sc = new SqlChars("LINESTRING(" + NWLon + " " + midY + ", " + SELon + " " + midY + ")");
            SqlGeometry latLine = SqlGeometry.STGeomFromText(sc, 4326);
            SqlGeometry latLineIntersect = latLine.STIntersection(geom);
            if (latLineIntersect.STNumGeometries() > 1) //handle multi-linestrings
            {
                double longestLineMeasure=0.0;
                SqlGeometry lineSegmentToKeep = new SqlGeometry();
                int geometryCount = (int)latLineIntersect.STNumGeometries();
                for (int geomIndex = 1; geomIndex <= latLineIntersect.STNumGeometries(); geomIndex++)
                {
                    SqlGeometry childGeom = latLineIntersect.STGeometryN(geomIndex);
                    if (childGeom.STLength() > longestLineMeasure)
                    {
                        lineSegmentToKeep = childGeom;
                        longestLineMeasure = (double)childGeom.STLength();
                    }
                }
                latLineIntersect = lineSegmentToKeep;
            }
            sc = new SqlChars("LINESTRING(" + midX + " " + NWLat + ", " + midX + " " + SELat + ")");
            SqlGeometry lonLine = SqlGeometry.STGeomFromText(sc, 4326);
            SqlGeometry lonLineIntersect = lonLine.STIntersection(geom);
            if (lonLineIntersect.STNumGeometries() > 1) //handle multi-linestrings
            {
                double longestLineMeasure = 0.0;
                SqlGeometry lineSegmentToKeep = new SqlGeometry();
                int geometryCount = (int)lonLineIntersect.STNumGeometries();
                for (int geomIndex = 1; geomIndex <= lonLineIntersect.STNumGeometries(); geomIndex++)
                {
                    SqlGeometry childGeom = lonLineIntersect.STGeometryN(geomIndex);
                    if (childGeom.STLength() > longestLineMeasure)
                    {
                        lineSegmentToKeep = childGeom;
                        longestLineMeasure = (double)childGeom.STLength();
                    }
                }
                lonLineIntersect = lineSegmentToKeep;
            }
            if (latLineIntersect.STLength() > lonLineIntersect.STLength())
            {
                midY = (((double)latLineIntersect.STStartPoint().STY - (double)latLineIntersect.STEndPoint().STY) / 2) + (double)latLineIntersect.STEndPoint().STY;
                midX = (((double)latLineIntersect.STStartPoint().STX - (double)latLineIntersect.STEndPoint().STX) / 2) + (double)latLineIntersect.STEndPoint().STX;
                sc = new SqlChars("POINT(" + midX + " " + midY + ")");
                labelPtGeom = SqlGeometry.STGeomFromText(sc, 4326);
            }
            else
            {
                midY = (((double)lonLineIntersect.STStartPoint().STY - (double)lonLineIntersect.STEndPoint().STY) / 2) + (double)lonLineIntersect.STEndPoint().STY;
                midX = (((double)lonLineIntersect.STStartPoint().STX - (double)lonLineIntersect.STEndPoint().STX) / 2) + (double)lonLineIntersect.STEndPoint().STX;
                sc = new SqlChars("POINT(" + midX + " " + midY + ")");
                labelPtGeom = SqlGeometry.STGeomFromText(sc, 4326);
            }
            if (labelPtGeom.STWithin(geom))
            {
                returnGeom = labelPtGeom;
            }
            else
            {
                returnGeom = geom.STCentroid();
            }
            returnPoint.latitude = (double)returnGeom.STPointN(1).STY;
            returnPoint.longitude = (double)returnGeom.STPointN(1).STX;
            return returnPoint;
        }


        public static SqlGeometry CreateNewGeomFromReducedShape(List<Polygon> polygonsCleaned)
        {
            SqlGeometry newGeom = new SqlGeometry();
            StringBuilder sb = new StringBuilder();
            if (polygonsCleaned.Count() > 1)
            {
                sb.Append("MULTIPOLYGON((");
            }
            else if (polygonsCleaned.Count() == 1)// single poly
            {
                sb.Append("POLYGON(");
            }
            else //is empty return empty geometry
            {
                return newGeom;
            }
            int polyInx = 0;
            foreach (Polygon poly in polygonsCleaned)
            {
                if (polyInx == 0)
                {
                    sb.Append("(");
                }
                else
                {
                    sb.Append(",(");
                }
                int ptInx = 01;
                foreach (SqlGeometry pt in poly.polyPoints) //exterior
                {
                    if (ptInx < poly.polyPoints.Count)
                    {
                        sb.Append(pt.STX.ToString() + " " + pt.STY.ToString() + ",");
                    }
                    else //last one no comma
                    {
                        sb.Append(pt.STX.ToString() + " " + pt.STY.ToString());
                    }
                    ptInx++;
                }
                sb.Append(")");
                foreach (interiorRing ir in poly.irRingList)
                {
                    sb.Append(",(");
                    ptInx = 01;
                    foreach (SqlGeometry pt in ir.irPoints)
                    {
                        if (ptInx < ir.irPoints.Count)
                        {
                            sb.Append(pt.STX.ToString() + " " + pt.STY.ToString() + ",");
                        }
                        else //last one no comma
                        {
                            sb.Append(pt.STX.ToString() + " " + pt.STY.ToString());
                        }
                        ptInx++;
                    }
                    sb.Append(")");
                }
                polyInx++;
            }
            if (polygonsCleaned.Count() > 1)
            {
                sb.Append("))");
            }
            else // single poly
            {
                sb.Append(")");
            }
            SqlChars sc = new SqlChars(sb.ToString());
            newGeom = SqlGeometry.STGeomFromText(sc, 4326);
            
            return newGeom;
        }

        
        public static SqlGeometry UnionSQLGeometries(List<SqlGeometry> geometryList)
        {
            List<SqlGeometry> unionedGeometryList = new List<SqlGeometry>();

            int batchSize = 2;
            for (int listIndex = 0; listIndex < geometryList.Count; )
            {
                SqlGeometry unionedGeometry = new SqlGeometry();

                for (int j = 0; j < batchSize; j++)
                {
                    if (listIndex >= geometryList.Count)
                    {
                        break;
                    }
                    else
                    {
                        if (j == 0)
                        {
                            unionedGeometry = geometryList[listIndex];
                        }
                        else
                        {
                            unionedGeometry = unionedGeometry.STUnion(geometryList[listIndex]);
                        }
                        listIndex++;
                    }
                }
                unionedGeometryList.Add(unionedGeometry);
            }

            if (unionedGeometryList.Count > 1)
            {
                return UnionSQLGeometries(unionedGeometryList);
            }
            else if (unionedGeometryList.Count == 1)
            {
                return unionedGeometryList.First();
            }
            else
            {
                // got here because we were given an empty list, so return an empty shape
                return new SqlGeometry();
            }
        }

        public static SqlGeometry RemoveSlivers(SqlGeometry geom)
        {
            SqlGeometry finalGeom = new SqlGeometry();

            int includedShapeCount = 0;

            //debug
            int geometryCount = (int)geom.STNumGeometries();

            // this could be a Geometry collection, MultiPolygon, Polygon, MultiPolyline(unlikely), Polyline(unlikely), MultiPoint(unlikely) or Point (unlikely)
            for (int geomIndex = 1; geomIndex <= geom.STNumGeometries(); geomIndex++)
            {
                SqlGeometry childGeom = geom.STGeometryN(geomIndex);

                //debug

                string childGeomType = (string)childGeom.STGeometryType();
                double childGeomArea = (double)childGeom.STArea();

                if (((string)childGeom.STGeometryType() == "GeometryCollection")
                    | ((string)childGeom.STGeometryType() == "MultiPolygon")
                    | ((string)childGeom.STGeometryType() == "Polygon"))
                {
                    SqlGeometry newGeom = new SqlGeometry();

                    if (childGeom.STNumGeometries() == 1)
                    {
                        newGeom = RemoveGeometryInteriorRings(childGeom);
                    }
                    else
                    {
                        newGeom = RemoveSlivers(childGeom);
                    }

                    SqlGeometry tempGeom;
                    if (includedShapeCount == 0)
                    {
                        tempGeom = newGeom;
                        includedShapeCount++;
                    }
                    else
                    {
                        tempGeom = finalGeom.STUnion(newGeom);
                    }

                    if (tempGeom.IsNull)
                    {
                        // STUnion returned null
                        finalGeom = geom;
                    }
                    else
                    {
                        finalGeom = tempGeom;
                    }
                }
            }
            return finalGeom;
        }

        public static SqlGeometry RemoveGeometryInteriorRings(SqlGeometry geom)
        {
            // stupid exterior "ring" come out as a stupid linestring - need to turn it into a stupid polygon - one stupid point at a time
            SqlGeometry finalGeom = MakeValidPolygon(geom.STExteriorRing());

            bool val = (bool)finalGeom.STIsValid();
            string txt = finalGeom.ToString();

            // now need to include any interior rings that are not insignificant slivers
            int interiorRingCount = (int)geom.STNumInteriorRing();
            for (int ringIndex = 1; ringIndex <= interiorRingCount; ringIndex++)
            {
                // all interior rings come out as linestrings too, need to make them polygons
                if (geom.STInteriorRingN(ringIndex).STNumPoints() > 2)
                {
                    SqlGeometry ring = MakeValidPolygon(geom.STInteriorRingN(ringIndex));
                    SqlGeography ringGeog = SqlGeography.STGeomFromText(ring.STAsText(), 4326);
                    double ringArea = (double)ringGeog.STArea();
                    if (ringArea > .0000001)
                    {
                        finalGeom = finalGeom.STDifference(ring);
                    }
                    else
                    {
                        // ha, dropped a sliver!
                        //int i = 1;
                    }
                }
            }

            return finalGeom;
        }


        private static SqlGeometry MakeValidPolygon(SqlGeometry ring)
        {
            SqlGeometryBuilder gb = new SqlGeometryBuilder();
            gb.SetSrid(4326);
            gb.BeginGeometry(OpenGisGeometryType.Polygon);

            int startIndex = 2;
            int endIndex = (int)ring.STNumPoints();
            int step = 1;
            double startPointX = (double)ring.STStartPoint().STX;
            double startPointY = (double)ring.STStartPoint().STY;
            gb.BeginFigure(startPointX, startPointY);
            for (int pIndex = startIndex; pIndex <= endIndex; pIndex += step)
            {
                gb.AddLine((double)ring.STPointN(pIndex).STX, (double)ring.STPointN(pIndex).STY);
            }
            gb.AddLine(startPointX, startPointY);
            gb.EndFigure();
            gb.EndGeometry();
            return gb.ConstructedGeometry.MakeValid();
        }


        private static List<PointDTO> GetExteriorRing(SqlGeometry sqlGeom)
        {
            List<PointDTO> newExteriorRing = new List<PointDTO>();
            SqlGeometry exteriorRing = sqlGeom.STExteriorRing();
            if (exteriorRing.STNumPoints() > 2)
            {
                for (int pIndex = 1; pIndex <= (int)exteriorRing.STNumPoints(); pIndex++)
                {
                    newExteriorRing.Add(new PointDTO((double)exteriorRing.STPointN(pIndex).STY, (double)exteriorRing.STPointN(pIndex).STX));
                }
            }
            return newExteriorRing;
        }

        private static List<List<PointDTO>> GetInteriorRings(SqlGeometry sqlGeom)
        {
            List<List<PointDTO>> newInteriorRingsList = new List<List<PointDTO>>();

            // now include the interior rings (if any)
            int interiorRingCount = (int)sqlGeom.STNumInteriorRing();
            for (int ringIndex = 1; ringIndex <= interiorRingCount; ringIndex++)
            {
                SqlGeometry interiorRing = sqlGeom.STInteriorRingN(ringIndex);
                if (interiorRing.STNumPoints() > 2)
                {
                    List<PointDTO> newInteriorList = new List<PointDTO>();
                    for (int pIndex = 1; pIndex <= (int)interiorRing.STNumPoints(); pIndex++)
                    {
                        newInteriorList.Add(new PointDTO((double)interiorRing.STPointN(pIndex).STY, (double)interiorRing.STPointN(pIndex).STX));
                    }
                    newInteriorRingsList.Add(newInteriorList);
                }
            }
            return newInteriorRingsList;
        }

        internal static MapMultiShapeDTO ToMapMultiShapeDTO(SqlGeometry sqlGeom)
        {
            MapMultiShapeDTO mapShape = new MapMultiShapeDTO();
            MapMultiShapeType newShapeType = MapMultiShapeType.Empty;
            BuildMapMultiShapeEntry(sqlGeom, out newShapeType, mapShape.shapeList);
            mapShape.shapeType = newShapeType;
            return mapShape;
        }

        internal static void BuildMapMultiShapeEntry(SqlGeometry sqlGeom, out MapMultiShapeType parentShapeType, List<MapMultiShapeEntryDTO> mapShapeList)
        {
            MapMultiShapeEntryDTO newMapShape = new MapMultiShapeEntryDTO();

            parentShapeType = MapMultiShapeType.Empty;
            string sqlShapeType = sqlGeom.STGeometryType().ToString().ToUpper();

            if (sqlShapeType == MapMultiShapeType.Polygon.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.Polygon;
                mapShapeList.Add(ToMapMultiShapePolygon(sqlGeom));
            }

            else if (sqlShapeType == MapMultiShapeType.MultiPolygon.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.MultiPolygon;

                int shapeCount = (int) sqlGeom.STNumGeometries();
                for ( int shapeIndex = 1; shapeIndex <= shapeCount; shapeIndex++) 
                {
                    MapMultiShapeType subShapeType = MapMultiShapeType.Empty; // this is ignored for the sub shapes
                    SqlGeometry sqlSubShape = sqlGeom.STGeometryN(shapeIndex);
                    BuildMapMultiShapeEntry(sqlSubShape, out subShapeType, mapShapeList);
                }
            }

            else if (sqlShapeType == MapMultiShapeType.LineString.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.LineString;
                mapShapeList.Add(ToMapMultiShapeLineString(sqlGeom));
            }

            else if (sqlShapeType == MapMultiShapeType.MultiLineString.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.MultiLineString;

                int shapeCount = (int)sqlGeom.STNumGeometries();
                for (int shapeIndex = 1; shapeIndex <= shapeCount; shapeIndex++)
                {
                    MapMultiShapeType subShapeType = MapMultiShapeType.Empty; // this is ignored for the sub shapes
                    SqlGeometry sqlSubShape = sqlGeom.STGeometryN(shapeIndex);
                    BuildMapMultiShapeEntry(sqlSubShape, out subShapeType, mapShapeList);
                }
            }

            else if (sqlShapeType == MapMultiShapeType.Point.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.Point;
                mapShapeList.Add(ToMapMultiShapePoint(sqlGeom));
            }

            else if (sqlShapeType == MapMultiShapeType.GeometricCollection.ToString().ToUpper())
            {
                parentShapeType = MapMultiShapeType.GeometricCollection;

                int shapeCount = (int)sqlGeom.STNumGeometries();
                for (int shapeIndex = 1; shapeIndex <= shapeCount; shapeIndex++)
                {
                    MapMultiShapeType subShapeType = MapMultiShapeType.Empty; // this is ignored for the sub shapes
                    SqlGeometry sqlSubShape = sqlGeom.STGeometryN(shapeIndex);
                    BuildMapMultiShapeEntry(sqlSubShape, out subShapeType, mapShapeList);
                }

            }

            else
            {
                // ignore the shape, we don't understand it
                //todo: Log that SQLGeometryTools.ToMapMultiShapeDTO can't figure out what shape a sqlGeometry is
            }

            return;
        }

        private static MapMultiShapeEntryDTO ToMapMultiShapePolygon(SqlGeometry sqlGeom)
        {
            MapMultiShapeEntryDTO shape = new MapMultiShapeEntryDTO();
            shape.shapeType = MapMultiShapeType.Polygon;
            shape.ExteriorRing = GetExteriorRing(sqlGeom);
            shape.InteriorRings = GetInteriorRings(sqlGeom);
            return shape;
        }

        private static MapMultiShapeEntryDTO ToMapMultiShapeLineString(SqlGeometry sqlGeom)
        {
            MapMultiShapeEntryDTO shape = new MapMultiShapeEntryDTO();
            shape.shapeType = MapMultiShapeType.LineString;
            shape.ExteriorRing = GetExteriorRing(sqlGeom);
            return shape;
        }

        private static MapMultiShapeEntryDTO ToMapMultiShapePoint(SqlGeometry sqlGeom)
        {
            MapMultiShapeEntryDTO shape = new MapMultiShapeEntryDTO();
            shape.shapeType = MapMultiShapeType.Point;
            shape.ExteriorRing = GetExteriorRing(sqlGeom);
            return shape;
        }

        //private static List<PointDTO> RemoveDuplicatePoints(List<PointDTO> pointList)
        //{
        //    const int comparisonInt = 100000;
        //    int ndx = 0;
        //    while (ndx < pointList.Count - 1)
        //    {
        //        if (
        //             (Convert.ToInt32(pointList[ndx].latitude*comparisonInt) == (Convert.ToInt32(pointList[ndx + 1].latitude*comparisonInt)))
        //            &(Convert.ToInt32(pointList[ndx].longitude*comparisonInt) == (Convert.ToInt32(pointList[ndx + 1].longitude*comparisonInt)))
        //           )
        //        {
        //            pointList.RemoveAt(ndx);
        //        }
        //        else
        //        {
        //            ndx++;
        //        }

        //    }
        //    return pointList;
        //}


    }
}
